1 /** 2 * Rotation manipulation feature 3 * 4 * License: 5 * Copyright Devisualization (Richard Andrew Cattermole) 2014 - 2017. 6 * Distributed under the Boost Software License, Version 1.0. 7 * (See accompanying file LICENSE_1_0.txt or copy at 8 * http://www.boost.org/LICENSE_1_0.txt) 9 */ 10 module devisualization.image.manipulation.rotation; 11 import devisualization.image.interfaces; 12 import devisualization.image.primitives : isImage, ImageColor, isPixelRange, PixelRangeColor; 13 import std.experimental.color : isColor; 14 import stdx.allocator : make, theAllocator, IAllocator; 15 16 /// 17 enum RotateDirection : bool { 18 /// 19 ClockWise, 20 21 /// 22 AntiClockWise 23 } 24 25 /* 26 * Set rotation 90 27 */ 28 29 /** 30 * Rotates an image 90 degrees in place. 31 * 32 * Params: 33 * from = The image to rotate 34 * direction = The direction to rotate 35 * 36 * Returns: 37 * The input image for chainability reasons. 38 */ 39 Image rotate90(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise) if (isImage!Image) { 40 // 0 => 1 41 // 1 => 2 42 // 2 => 3 43 // 3 => 0 44 45 return perform4WaySwap(from, direction, 1); 46 } 47 48 /** 49 * Rotates an image 90 degrees 50 * 51 * Wraps the input range varient of this to take an image instead. 52 * 53 * Params: 54 * image = The image to rotate 55 * direction = The direction to rotate 56 * allocator = The allocator to allocate an input range from 57 * 58 * Returns: 59 * An input range that has an ElementType of PixelPoint. 60 */ 61 auto rotate90Range(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise, IAllocator allocator = theAllocator()) if (isImage!Image) { 62 return rotate90Range(from.rangeOf(allocator), direction); 63 } 64 65 /** 66 * Rotates an image 90 degrees 67 * 68 * Params: 69 * from = Input range to take pixels from 70 * direction = The direction to rotate 71 * allocator = The allocator to allocate an input range from 72 * 73 * Returns: 74 * An input range that has an ElementType of PixelPoint. 75 */ 76 auto rotate90Range(IR)(IR from, RotateDirection direction = RotateDirection.ClockWise) if (isPixelRange!IR) { 77 return RotateN!(PixelRangeColor!IR, IR)(from, 1, direction); 78 } 79 80 /* 81 * Set rotation 180 82 */ 83 84 /** 85 * Rotates an image 180 degrees in place. 86 * 87 * Params: 88 * from = The image to rotate 89 * direction = The direction to rotate 90 * 91 * Returns: 92 * The input image for chainability reasons. 93 */ 94 Image rotate180(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise) if (isImage!Image) { 95 // 0 => 2 96 // 1 => 3 97 // 2 => 0 98 // 3 => 1 99 100 return perform4WaySwap(from, direction, 2); 101 } 102 103 /** 104 * Rotates an image 180 degrees 105 * 106 * Wraps the input range varient of this to take an image instead. 107 * 108 * Params: 109 * image = The image to rotate 110 * direction = The direction to rotate 111 * allocator = The allocator to allocate an input range from 112 * 113 * Returns: 114 * An input range that has an ElementType of PixelPoint. 115 */ 116 auto rotate180Range(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise, IAllocator allocator = theAllocator()) if (isImage!Image) { 117 return rotate180Range(from.rangeOf(allocator), direction); 118 } 119 120 /** 121 * Rotates an image 180 degrees 122 * 123 * Params: 124 * from = Input range to take pixels from 125 * direction = The direction to rotate 126 * allocator = The allocator to allocate an input range from 127 * 128 * Returns: 129 * An input range that has an ElementType of PixelPoint. 130 */ 131 auto rotate180Range(IR)(IR from, RotateDirection direction = RotateDirection.ClockWise) if (isPixelRange!IR) { 132 return RotateN!(PixelRangeColor!IR, IR)(from, 2, direction); 133 } 134 135 /* 136 * Set rotation 270 137 */ 138 139 /** 140 * Rotates an image 270 degrees in place. 141 * 142 * Params: 143 * from = The image to rotate 144 * direction = The direction to rotate 145 * 146 * Returns: 147 * The input image for chainability reasons. 148 */ 149 Image rotate270(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise) if (isImage!Image) { 150 // 0 => 3 151 // 1 => 0 152 // 2 => 1 153 // 3 => 2 154 155 return perform4WaySwap(from, direction, 3); 156 } 157 158 /** 159 * Rotates an image 270 degrees 160 * 161 * Wraps the input range varient of this to take an image instead. 162 * 163 * Params: 164 * image = The image to rotate 165 * direction = The direction to rotate 166 * allocator = The allocator to allocate an input range from 167 * 168 * Returns: 169 * An input range that has an ElementType of PixelPoint. 170 */ 171 auto rotate270Range(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise, IAllocator allocator = theAllocator()) if (isImage!Image) { 172 return rotate270Range(from.rangeOf(allocator), direction); 173 } 174 175 /** 176 * Rotates an image 270 degrees 177 * 178 * Params: 179 * from = Input range to take pixels from 180 * direction = The direction to rotate 181 * allocator = The allocator to allocate an input range from 182 * 183 * Returns: 184 * An input range that has an ElementType of PixelPoint. 185 */ 186 auto rotate270Range(IR)(IR from, RotateDirection direction = RotateDirection.ClockWise) if (isPixelRange!IR) { 187 return RotateN!(PixelRangeColor!IR, IR)(from, 3, direction); 188 } 189 190 /* 191 * Abituary rotation 192 */ 193 194 /** 195 * Simple shear+clip rotation. 196 * 197 * Uses radians. 198 */ 199 Image rotate(Impl, Image)(Image from, double by, IAllocator allocator = theAllocator()) if (isImage!Impl && isImage!Image) 200 in { 201 assert(by <= D_PI); 202 } body { 203 import std.math : cos, sin; 204 import std.typecons : tuple; 205 206 if (by > 0) { 207 while(by > D_PI) { 208 by -= D_PI; 209 } 210 } else { 211 while (by < -D_PI) { 212 by += D_PI; 213 } 214 } 215 216 auto newSize = calculateNewSize(from.width, from.height, by); 217 Impl ret = allocator.make!Impl(newSize[0], newSize[1], allocator); 218 219 foreach(toX; 0 .. newSize[0]) { 220 foreach(toY; 0 .. newSize[1]) { 221 auto destCoord = oldCoord(toX, toY, by); 222 223 ret[destCoord.x, destCoord.y] = from[destCoord.x, destCoord.y]; 224 } 225 } 226 227 return ret; 228 } 229 230 private { 231 import std.math : PI_2, PI, PI_4; 232 import std.typecons : Tuple, tuple; 233 234 enum PI_34 = 3 * PI_4; 235 enum D_PI = 2 * PI; 236 237 // TODO: unittests! 238 Tuple!(size_t, size_t) calculateNewSize(size_t oldWidth, size_t oldHeight, double by) { 239 import std.math : cos, sin; 240 241 double newWidth, newHeight; 242 243 //L * cos(t) + H 244 newWidth = oldWidth * cos(by) + oldHeight; 245 //L * sin(t) + H 246 newHeight = oldWidth * sin(by) + oldHeight; 247 248 double by2; 249 if (by <= PI_2) //(90 – t) 250 by2 = PI_2 - by; 251 else if (by <= PI) //(180 – t) 252 by2 = PI - by; 253 else if (by <= PI_34) //(t – 180) 254 by2 = by - PI_34; 255 else if (by <= D_PI) //(360 – t) 256 by2 = D_PI - by; 257 258 // * cos(90 – t) 259 newWidth *= cos(by2); 260 // * sin(90 – t) 261 newHeight *= sin(by2); 262 263 return tuple(cast(size_t)newWidth, cast(size_t)newHeight); 264 } 265 266 // TODO: unittests! 267 Tuple!(size_t, size_t) oldCoord(size_t toX, size_t toY, double by) { 268 import std.math : cos, sin; 269 270 double a = cos(by); 271 double b = sin(by); 272 double determinant = a*a-b*-b; 273 274 double x = (toX * a) + (toY * b); 275 double y = (toX * -b) + (toY * a); 276 x /= determinant; 277 y /= determinant; 278 279 // FIXME 280 assert(x >= 0); 281 assert(y >= 0); 282 283 return tuple(cast(size_t)x, cast(size_t)y); 284 } 285 286 size_t[4][2] coords4WaySwap(size_t width, size_t height, size_t x, size_t y, RotateDirection direction) @safe 287 in { 288 import std.math : floor; 289 import std.exception : enforce; 290 291 enforce(x <= floor(width / 2f)); 292 enforce(y <= floor(height / 2f)); 293 } body { 294 if (direction == RotateDirection.ClockWise) { 295 // 0 1 2 3 296 return [[ 297 x, 298 y, 299 width - (x+1), 300 width - (y+1) 301 ], [ 302 y, 303 x, 304 height - (y+1), 305 height - (x+1) 306 ]]; 307 } else if (direction == RotateDirection.AntiClockWise) { 308 // 2 3 0 1 309 310 return [[ 311 width - (x+1), 312 width - (y+1), 313 x, 314 y 315 ], [ 316 height - (y+1), 317 height - (x+1), 318 y, 319 x 320 ]]; 321 } 322 323 assert(0); 324 } 325 326 unittest { 327 import std.exception : assertThrown, assertNotThrown; 328 329 // contract checks 330 331 assertThrown(coords4WaySwap(3, 3, 2, 1, RotateDirection.ClockWise)); 332 assertNotThrown(coords4WaySwap(3, 3, 1, 0, RotateDirection.ClockWise)); 333 assertThrown(coords4WaySwap(3, 3, 2, 1, RotateDirection.ClockWise)); 334 } 335 336 unittest { 337 size_t[4][2] v; 338 339 // if condition check 340 341 v = coords4WaySwap(3, 3, 0, 1, RotateDirection.ClockWise); 342 assert(v[0][0] == 0); 343 assert(v[0][1] == 1); 344 assert(v[1][0] == 1); 345 assert(v[1][1] == 0); 346 347 assert(v[0][2] == 2); 348 assert(v[0][3] == 1); 349 assert(v[1][2] == 1); 350 assert(v[1][3] == 2); 351 } 352 353 unittest { 354 size_t[4][2] v; 355 356 // clockwise 357 358 v = coords4WaySwap(3, 3, 0, 1, RotateDirection.AntiClockWise); 359 assert(v[0][2] == 0); 360 assert(v[0][3] == 1); 361 assert(v[1][2] == 1); 362 assert(v[1][3] == 0); 363 364 assert(v[0][0] == 2); 365 assert(v[0][1] == 1); 366 assert(v[1][0] == 1); 367 assert(v[1][1] == 2); 368 } 369 370 // TODO: unittests! 371 Image perform4WaySwap(Image)(Image from, RotateDirection direction, ubyte idxAdd) if (isImage!Image) { 372 foreach(x; 0 .. (from.width / 2)) { 373 foreach(y; 0 .. cast(size_t)(from.height / 2)) { 374 size_t[4][2] coords = coords4WaySwap(from.width, from.height, x, y, direction); 375 376 Color[4] temp = [ 377 from[coords[0][0], coords[1][0]], from[coords[0][1], coords[1][1]], 378 from[coords[0][2], coords[1][2]], from[coords[0][3], coords[1][3]] 379 ]; 380 381 ubyte nidx; 382 383 nidx = (0 + idxAdd) % 4; 384 from[coords[0][nidx], coords[1][nidx]] = temp[0]; 385 nidx = (1 + idxAdd) % 4; 386 from[coords[0][nidx], coords[1][nidx]] = temp[1]; 387 nidx = (2 + idxAdd) % 4; 388 from[coords[0][nidx], coords[1][nidx]] = temp[2]; 389 nidx = (3 + idxAdd) % 4; 390 from[coords[0][nidx], coords[1][nidx]] = temp[3]; 391 } 392 } 393 394 return from; 395 } 396 397 // TODO: unittests 398 struct RotateN(Color, IRRange) { 399 IRRange input; 400 PixelPoint!Color current; 401 ubyte idxToAdd; 402 RotateDirection direction; 403 404 this(IRRange input, ubyte idxToAdd, RotateDirection direction) { 405 this.input = input; 406 this.idxToAdd = idxToAdd; 407 this.direction = direction; 408 popFront; 409 } 410 411 @property { 412 PixelPoint!Color front() { 413 return current; 414 } 415 416 bool empty() { 417 return input.empty; 418 } 419 } 420 421 void popFront() { 422 if (!empty) { 423 PixelPoint got = input.front; 424 425 size_t[4][2] coords = coords4WaySwap(got.width, got.height, got.x, got.y, direction); 426 ubyte corner; 427 428 if (got.x < got.width / 2) { 429 if (got.y < got.height / 2) { 430 corner = 0; 431 } else { 432 corner = 3; 433 } 434 } else { 435 if (got.y < got.height / 2) { 436 corner = 1; 437 } else { 438 corner = 2; 439 } 440 } 441 442 ubyte nidx; 443 nidx = (corner + idxAdd) % 4; 444 current = PixelPoint!Color(got.value, coords[0][nidx], coords[1][nidx], got.width, got.height); 445 446 input.popFront; 447 } 448 } 449 } 450 }